--[[
Name: School-2.0
Revision: $Rev: 4 $
Author: Kodewulf (kodewulf@gmail.com)
Website: http://www.wowace.com
Description: Library providing information regarding trainers.
Dependencies: AceLibrary, AceOO-2.0, AceLocale-2.2, AceEvent-2.0
]]

local MAJOR_VERSION = "School-2.0"
local MINOR_VERSION = "$Rev: 4 $"

if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end
if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end 
if not AceLibrary:HasInstance("AceLocale-2.2") then error(MAJOR_VERSION .. " requires AceLocale-2.2") end
if not AceLibrary:HasInstance("AceEvent-2.0") then error(MAJOR_VERSION .. " requires AceEvent-2.0") end

local function print(text, r, g, b, frame, delay)
	(frame or DEFAULT_CHAT_FRAME):AddMessage(text, r, g, b, 1, delay or 5)
end

local AceOO = AceLibrary:GetInstance("AceOO-2.0")
-- local AceDebug = AceOO.Mixin {"Debug", "CustomDebug", "IsDebugging", "SetDebugging", "SetDebugLevel", "LevelDebug", "CustomLevelDebug", "GetDebugLevel"}

local AceOO = AceLibrary:GetInstance("AceOO-2.0")
local SchoolLib = AceOO.Mixin {
    "GotoSchool",
    "Enroll",
    "Rollcall"
}

SchoolLib.data = {}
SchoolLib.storage = {}
SchoolLib.lookup = {}

SchoolLib.debugging = true

if not AceLibrary:HasInstance("AceDebug-2.0") then
    SchoolLib.CustomDebug = AceLibrary.CustomDebug
else
    function SchoolLib:CustomDebug(r, g, b, frame, delay, a1, ...)
        local tmp = {}
        
    	if not self.debugging then
    		return
    	end

    	local output = ("|cff7fff7f(DEBUG) %s:[%s.%3d]|r"):format(tostring(self), date("%H:%M:%S"), (GetTime() % 1) * 1000)
    	
    	a1 = tostring(a1)
    	if a1:find("%%") and select('#', ...) >= 1 then
    		for i = 1, select('#', ...) do
    			tmp[i] = tostring((select(i, ...)))
    		end
    		output = output .. " " .. a1:format(unpack(tmp))
    		for i = 1, select('#', ...) do
    			tmp[i] = nil
    		end
    	else
    		-- This block dynamically rebuilds the tmp array stopping on the first nil.
    		tmp[1] = output
    		tmp[2] = a1
    		for i = 1, select('#', ...) do
    			tmp[i+2] = tostring((select(i, ...)))
    		end
    		
    		output = table.concat(tmp, " ")
    		
    		for i = 1, select('#', ...) + 2 do
    			tmp[i] = nil
    		end
    	end

    	print(output, r, g, b, frame or self.debugFrame, delay)
    end
end

function SchoolLib:Debug(...)
	SchoolLib.CustomDebug(self, nil, nil, nil, nil, nil, ...)
end

if AceLibrary:HasInstance("Gratuity-2.0") then SchoolLib.tooltip = AceLibrary("Gratuity-2.0") end

if AceLibrary:HasInstance("AceDB-2.0") then
    SchoolLib.charid = AceLibrary("AceDB-2.0").CHAR_ID
    SchoolLib.realmid = AceLibrary("AceDB-2.0").REALM_ID
    SchoolLib.classid = AceLibrary("AceDB-2.0").CLASS_ID
    SchoolLib.faction = AceLibrary("AceDB-2.0").FACTION
    SchoolLib.realm = AceLibrary("AceDB-2.0").REALM
    SchoolLib.name = AceLibrary("AceDB-2.0").NAME
else
    SchoolLib.charid = ""
    SchoolLib.realmid = ""
    SchoolLib.classid = ""
    SchoolLib.faction = ""
    SchoolLib.realm = ""
    SchoolLib.name = UnitName("player")
end

-------------------------------------------------------------------------------
-- Localization
-------------------------------------------------------------------------------

--[[
SchoolLib.lang = AceLibrary("AceLocale-2.2"):new(MAJOR_VERSION)
SchoolLib.lang:RegisterTranslations("enUS", function() return {

	["Rebuild"] = true,
	["Rebuild % data."] = true,

    ["School"] = true,
	["Student"] = true,


	["Teacher"] = true,
} end)

-- SchoolLib.lang = SchoolLib.locale.lib
--]]

SchoolLib.babble = {}
SchoolLib.babble.active = true

SchoolLib.babble.lang = AceLibrary("AceLocale-2.2"):new(MAJOR_VERSION)
SchoolLib.babble.lang:RegisterTranslations("enUS", function() return {
	["Rebuild"] = true,
	["Rebuild % data."] = true,

    ["School"] = true,
	["Student"] = true,


	["Teacher"] = true,
} end)


if (AceLibrary:HasInstance("Babble-Class-2.2") and (SchoolLib.babble.active)) then SchoolLib.babble.class = AceLibrary("Babble-Class-2.2") end
if (AceLibrary:HasInstance("Babble-Race-2.2") and (SchoolLib.babble.active)) then SchoolLib.babble.race = AceLibrary("Babble-Race-2.2") end
if (AceLibrary:HasInstance("Babble-Spell-2.2") and (SchoolLib.babble.active)) then SchoolLib.babble.spell = AceLibrary("Babble-Spell-2.2") end
if (AceLibrary:HasInstance("Babble-SpellTree-2.2") and (SchoolLib.babble.active)) then SchoolLib.babble.spelltree = AceLibrary("Babble-SpellTree-2.2") end
if (AceLibrary:HasInstance("Babble-Zone-2.2") and (SchoolLib.babble.active)) then SchoolLib.babble.zone = AceLibrary("Babble-Zone-2.2") end

SchoolLib.babble.Lang = function(self, v)
    if (not SchoolLib.babble.lang[v]) then return v end
    return SchoolLib.babble.lang[v]
end

SchoolLib.babble.LangReverse = function(self, v)
    if (not SchoolLib.babble.lang:HasReverseTranslation(v)) then return v end
    return SchoolLib.babble.lang:ReverseTranslation(v)
end

SchoolLib.babble.Class = function(self, v)
    if (not SchoolLib.babble.active) or
        (not SchoolLib.babble.class) or
        (not SchoolLib.babble.HasTranslation(v)) then
        return v
    end
    return SchoolLib.babble.class:GetTranslation(v)
end

SchoolLib.babble.ClassReverse = function(self, v)
    if (not SchoolLib.babble.active) or 
        (not SchoolLib.babble.class) or 
        (not SchoolLib.babble.class:HasReverseTranslation(v)) or
        (not SchoolLib.babble.class:GetReverseTranslation(v)) then
        return v
    end
    
    return SchoolLib.babble.class:GetReverseTranslation(v)
end

SchoolLib.babble.Race = function(self, v)
    if (not SchoolLib.babble.active) or
        (not SchoolLib.babble.race) or
        (not SchoolLib.babble.race:HasTranslation(v)) then
        return v
    end
    return SchoolLib.babble.race:GetTranslation(v)
end

SchoolLib.babble.RaceReverse = function(self, v)
    if (not SchoolLib.babble.active) or
        (not SchoolLib.babble.race) or
        (not SchoolLib.babble.race:HasReverseTranslation(v)) or
        (not SchoolLib.babble.race:GetReverseTranslation(v)) then
        return v
    end
    return SchoolLib.babble.race:GetReverseTranslation(v)
end
    

SchoolLib.babble.Spell = function(self, v)
    if (not SchoolLib.babble.active) or
        (not SchoolLib.babble.spell) or
        (not SchoolLib.babble.spell:HasTranslation(v)) then
        return v
    end
    return SchoolLib.babble.spell:GetTranslation(v)
end

SchoolLib.babble.SpellReverse = function(self, v)
    if (not SchoolLib.babble.active) or
        (not SchoolLib.babble.spell) or
        (not SchoolLib.babble.spell:HasReverseTranslation(v)) or
        (not SchoolLib.babble.spell:GetReverseTranslation(v)) then
        return v
    end
    return SchoolLib.babble.spell:GetReverseTranslation(v)
end

SchoolLib.babble.SpellTree = function(self, v)
    if (not SchoolLib.babble.active) or
        (not SchoolLib.babble.spelltree) or
        (not SchoolLib.babble.spelltree:HasTranslation(v)) then
        return v
    end
    return SchoolLib.babble.spelltree:GetTranslation(v)
end

SchoolLib.babble.SpellTreeReverse = function(self, v)
    if (not SchoolLib.babble.active) or
        (not SchoolLib.babble.spelltree) or
        (not SchoolLib.babble.spelltree:HasReverseTranslation(v)) or
        (not SchoolLib.babble.spelltree:GetReverseTranslation(v)) then
        return v
    end
    return SchoolLib.babble.spelltree:GetReverseTranslation(v)
end

SchoolLib.babble.Zone = function(self, v)
    if (not SchoolLib.babble.active) or
        (not SchoolLib.babble.zone) or
        (not SchoolLib.babble.zone:HasTranslation(v)) then
        return v
    end
    return SchoolLib.babble.zone:GetTranslation(v)
end

SchoolLib.babble.ZoneReverse = function(self, v)
    if (not SchoolLib.babble.active) or
        (not SchoolLib.babble.zone) or
        (not SchoolLib.babble.zone:HasReverseTranslation(v)) or
        (not SchoolLib.babble.zone:GetReverseTranslation(v)) then
        return v
    end
    return SchoolLib.babble.zone:GetReverseTranslation(v)
end

-------------------------------------------------------------------------------
-- Options
-------------------------------------------------------------------------------
SchoolLib.defaults = {
    ignoreOverrideLevelPlayer = true,
    ignoreOverrideLevelSkill = true,
    ignoreCost = true,
    totalCost = true,
    
    -- Debug
    overrideLevelPlayer = 20,
    ignoreOverrideLevelPlayer = false,
    
}

SchoolLib.options = {
    ignoreOverrideLevelPlayer = true,
    ignoreOverrideLevelSkill = true,
    ignoreCost = true,
    totalCost = true,
    includeKnown = false,
}

-------------------------------------------------------------------------------
-- Initialization
-------------------------------------------------------------------------------
function SchoolLib:OnInitialize()
end

-------------------------------------------------------------------------------
-- Public
-------------------------------------------------------------------------------
SchoolLib.matrix = {}

function SchoolLib:GetData()
end

function SchoolLib:GotoSchool()
    self:Debug("GotoSchool")
    self:BuildTeachers()
    self:BuildStudent()
    self:BuildSchool()
    self:TriggerEvent("School_Changed")
end

function SchoolLib:Enroll(db)
    if (not db) then error("Usage Enroll(db).") end
    if (type(db) ~= "table") then error("Usage Enroll(db).") end

    self.storage.data = db
    self.storage.lastupdate = time()
    
    if (not self.storage.data.player) then self.storage.data.player = {} end
    if (not self.storage.data.player.counters) then self.storage.data.player.counters = {} end
    if (not self.storage.data.school) then self.storage.data.school = {} end
    if (not self.storage.data.spells) then self.storage.data.spells = {} end
    if (not self.storage.data.teachers) then self.storage.data.teachers = {} end
    
    if AceLibrary:HasInstance("School-Data-2.0") then SchoolLib.matrix = AceLibrary("School-Data-2.0") end
end

function SchoolLib:Rollcall()
    if (self.rollcall) then return end

    
    self:RegisterEvent("TRAINER_SHOW", "Event_TrainerInfo", 2)
    self:RegisterEvent("TRAINER_CLOSED", "Event_TrainerSpells", 2)
    
    self:RegisterEvent("MERCHANT_SHOW", "Event_MerchantInfo", 2)
    self:RegisterEvent("MERCHANT_CLOSED", "Event_MerchantSpells", 2)
    
    self:RegisterEvent("CHARACTER_POINTS_CHANGED", "Event_CharacterPoints", 2)
    self:RegisterEvent("CHAT_MSG_SKILL", "Event_ChatMsgSkill", 5)
    self:RegisterEvent("SPELLS_CHANGED", "Event_Spells", 5)
    self:RegisterEvent("PLAYER_LEVEL_UP", "Event_PlayerLevelUp")

    self:RegisterEvent("PLAYER_ENTERING_WORLD", "Event_PlayerEnteringWorld")

    self:RegisterEvent("SchoolEvent_Build", "Event_SchoolBuild")
    self:RegisterEvent("SchoolEvent_TeacherChanged", "Event_SchoolTeacherChanged", 5)
    self:RegisterEvent("SchoolEvent_StudentChanged", "Event_SchoolStudentChanged", 5)

    self.data.lastupdate = time()
   
    self.rollcall = true
end


-------------------------------------------------------------------------------
-- Internal
-------------------------------------------------------------------------------
function SchoolLib:Clone(source)
    if (not source) then return end
    if (type(source) ~= table) then return source end
    
    local target = {}

    for k,v in pairs(source) do
        if (v ~= nil) then
            if (type(v) == "table") then
                target[k] = self:Clone(v)
            else
                target[k] = v
            end
        end
    end
    
    return target
end

function SchoolLib:BuildStudent()
    self.data.student = self:GetDataStudent()
    return self.data.student
end

function SchoolLib:BuildTeachers()
    self.data.teachers = self:GetDataTeachers()
    return self.data.teachers
end

function SchoolLib:BuildSchool()
    self:Debug("BuildSchool")
    -- self.data.school = self:GetDataSchool()
    -- return self.data.school
    
    if (not self.data.teachers) then return end
    if (not self.data.student) then return end
    
    local school = {}
    local teachers = self.data.teachers
    local student = self.data.student

    if (not teachers.charspells) then return end
    -- self:Debug("Found Character Spells data.")
    
    --[[
    if (teacher.counters) then
        self:Debug("spells: %s\ntrainers: %s\ncharacter spells: %s", teacher.counters.spells, teacher.counters.trainers, teacher.counters.charspells)
    end
    --]]
    
    student.level = UnitLevel("player")
    if (not self.options.ignoreOverrideLevelPlayer) then student.level = self.options.overrideLevelPlayer end
    student.level = tonumber(student.level)
    
    local includeKnown = self.options.includeKnown
    
    for k, v in pairs(teachers.charspells) do
        if (v ~= nil) then
            self:Debug("Key ==> " .. k)
        end
    end
    
end

-------------------------------------------------------------------------------
-- Scan student
-------------------------------------------------------------------------------
function SchoolLib:ScanStudentLevel()
    self.storage.data.player.level = UnitLevel("player")
end

function SchoolLib:ScanStudentMoney()
    self.storage.data.player.money = GetMoney()
end

function SchoolLib:ScanStudentCharacterPoints()
    local cp1, cp2 = UnitCharacterPoints("player");
    self.storage.data.player.charpoints = {}
    self.storage.data.player.charpoints.cp1 = cp1
    self.storage.data.player.charpoints.cp2 = cp2
end

function SchoolLib:ScanStudentSkills()
    self:Debug("LogSkillInfo")
    local skills = {}
    self.storage.data.player.counters.skills = 0
    
    for i = 1, GetNumSkillLines() do
        local skillName, header, isExpanded, skillRank, numTempPoints, skillModifier, skillMaxRank, isAbandonable, stepCost, rankCost, minLevel, skillCostType = GetSkillLineInfo(i);

        if (skillName ~= nil) and (not header) then
            local skill = {}
            skill.name = self.babble:SpellReverse(skillName)
            skill.rank = skillRank
            skill.maxrank = skillMaxRank
            skill.modifier = skillModifier
            skill.abandon = isAbandonable
            skill.level = minLevel
            skill.cost = {}
            skill.cost.step = stepCost
            skill.cost.rank = rankCost
            skill.cost.type = skillCostType
            
            
            skills[skill.name] = skill
            self.storage.data.player.counters.skills = self.storage.data.player.counters.skills + 1
        end
    end

    self.storage.data.player.skills = skills

    --[[
    for k,v in pairs(self.db.char.skills) do
        if (v ~= nil) then
            self:Debug("Key ==> %s\nValue ==> %s", k, v)
        end
    end
    --]]
end

function SchoolLib:ScanStudentProfessions()
    self:Debug("ScanStudentProfessions")

    local professions = {}
    self.storage.data.player.counters.professions = 0
    
    if (not self.storage.data.player.skills) then self:ScanStudentSkills() end
    local skills = self.storage.data.player.skills
    
    -- Alchemy
    -- Blacksmithing
    -- Cooking
    -- Enchanting
    -- Engineering
    -- First Aid
    -- Fishing
    -- Herbalism
    -- Jewelcrafting
    -- Leatherworking
    -- Mining
    -- Skinning
    -- Tailoring 

    local listProfessions = {}
    listProfessions.primary = {
                               ["Alchemy"] = true,
                               ["Blacksmithing"] = true,
                               ["Enchanting"] = true,
                               ["Enginering"] = true,
                               ["Herbalism"] = true,
                               ["Jewelcrafting"] = true,
                               ["Leatherworking"] = true,
                               ["Mining"] = true,
                               ["Skinning"] = true,
                               ["Tailoring"] = true,
    }
    listProfessions.secondary = {
                                 ["Cooking"] = true,
                                 ["First Aid"] = true,
                                 ["Fishing"] = true,
    }
    
    if (self.storage.data.player.professions ~= nil) then professions = self.storage.data.player.professions end

    -- if (not skills) then self:Debug("No skills found.") end
    -- if (not professions) then self:Debug("No professions found.") end
                             
    for k,v in pairs (skills) do
        if (v ~= nil) then
            local skill = v
            for lpk, lpv in pairs(listProfessions) do
                if (not professions[lpk]) then professions[lpk] = {} end
                if (not professions[lpk].count) then professions[lpk].count = 0 end       

                -- if (self.babble.active) and (self.babble.spell) then skill.name = self.babble.spell[skill.name] end
                skill.name = self.babble:SpellReverse(skill.name)
                
                if (not professions[lpk][skill.name]) then 
                    if (lpv[skill.name] ~= nil) then
                        professions[lpk][skill.name] = {}
                        professions[lpk][skill.name].max = skill.maxrank
                        professions[lpk][skill.name].rank = skill.rank
                        professions[lpk].count = professions[lpk].count + 1
                    end
                else
                    professions[lpk][skill.name].max = skill.maxrank
                    professions[lpk][skill.name].rank = skill.rank
                end
            end
        end
    end

    for k,v in pairs(professions) do
        if (v ~= nil) then
            if (v.count ~= nil) then
                self.storage.data.player.counters.professions = self.storage.data.player.counters.professions + v.count
            end
        end
    end
    
    
    --[[
    for k,v in pairs(professions) do
        if (v ~= nil) then
            for vk, vv in pairs(v) do
                if (vv ~= nil) then
                    self:Debug("Value Key ==> %s", vk)
                end
            end
        end
    end
    --]]
    
    self.storage.data.player.professions = professions
end

function SchoolLib:ScanStudentSpells()
    self:Debug("ScanStudentSpells")
    local spells = {}
    self.storage.data.player.counters.spells = 0

    local i = 1
    while true do
        local spell = {}
        
        spell.name, spell.rank = GetSpellName(i, BOOKTYPE_SPELL)
        if (not spell.name) then
            do break end
        end
        
        spell.icon = GetSpellTexture(i, BOOKTYPE_SPELL)
        spell.passive = IsPassiveSpell(i, BOOKTYPE_SPELL)
        
        if (not spells["player"]) then spells["player"] = {} end
        if (not spells["player"][i]) then spells["player"][i] = {} end

        spell.name = self.babble:SpellReverse(spell.name)
        
        spells["player"][i] = spell
        self.storage.data.player.counters.spells = self.storage.data.player.counters.spells + 1
       
        i = i + 1
    end
    
    local j = 1
    while true do
        local spell = {}
        
        spell.name, spell.rank = GetSpellName(j, BOOKTYPE_PET)
        if (not spell.name) then
            do break end
        end
        
        spell.icon = GetSpellTexture(j, BOOKTYPE_PET)
        spell.passive = IsPassiveSpell(j, BOOKTYPE_PET)
        
        if (not spells["pet"]) then spells["pet"] = {} end
        if (not spells["pet"][j]) then spells["pet"][j] = {} end

        spell.name = self.babble:SpellReverse(spell.name)
        
        spells["pet"][j] = spell
        self.storage.data.player.counters.spells = self.storage.data.player.counters.spells + 1
    
        j = j + 1
    end
    
    self.storage.data.player.spells = spells
end

function SchoolLib:ScanStudentTalents()
    self:Debug("ScanStudentTalents")
    local talents = {}
    self.storage.data.player.counters.talents = 0
    
    for t = 1, GetNumTalentTabs() do
        local numTalents = GetNumTalents(t)
    
        for i = 1, numTalents do
            nameTalent, icon, tier, column, currRank, maxRank = GetTalentInfo(t, i)
            
            if (nameTalent ~= nil) then
                local talent = {}
                
                talent.name = self.babble:SpellReverse(nameTalent)
                talent.icon = icon
                talent.tier = tier
                talent.column = column
                talent.rank = currRank
                talent.maxrank = maxRank
                
                talents[talent.name] = talent
                self.storage.data.player.counters.talents = self.storage.data.player.counters.talents + 1
            end
        end
    end
    
    self.storage.data.player.talents = talents
end

function SchoolLib:ScanStudentTradeskills()
end

function SchoolLib:RebuildStudent()
    self:Debug("RebuildStudent")

    -- Player level
    self.storage.data.player.level = nil
    self:ScanStudentLevel()
    
    -- Money
    self.storage.data.player.money = nil
    self:ScanStudentMoney()

    -- Character points
    self.storage.data.player.charpoints = nil
    self:ScanStudentCharacterPoints()

    -- Spells
    self.storage.data.player.spells = nil
    self:ScanStudentSpells()
    
    -- Talents
    self.storage.data.player.talents = nil
    self:ScanStudentTalents()

    -- Tradeskills
    self.storage.data.player.tradeskills = nil
    self:ScanStudentTradeskills()
    
    -- Skills
    self.storage.data.player.skills = nil
    self:ScanStudentSkills()
    
    -- Professions
    self.storage.data.player.professions = nil
    self:ScanStudentProfessions()
    
    self:TriggerEvent("School_Student_Changed")
    
    return self:GetDataStudent()
end

-------------------------------------------------------------------------------
-- Scan teacher
-------------------------------------------------------------------------------
function SchoolLib:ScanTeacherTrainerInfo()
    self:Debug("ScanTeacherTrainerInfo")
    
    local npc = {}
    npc.name = self:GetTeacherName()
    npc.zone = self:GetTeacherZone()
    
    local teachers_global = self:LoadTeachersByZone(npc.zone, true) or {}
    local teachers_local = self:Clone(self.storage.data.teachers) or {}
    
    if (not teachers_global[npc.name]) and (not teachers_local[npc.name]) then
        teachers_local[npc.name] = {}
        npc.type = self:GetTeacherType()
        npc.subzone = self:GetTeacherSubzone()
        npc.coords = {}
        npc.coords.x, npc.coords.y = GetPlayerMapPosition("player")

        --[[
        if (self:NPCIsTeacher(npc)) then
            self.storage.data.teachers[npc.name] = npc
        end
        --]]
        teachers_local[npc.name] = npc
    end
    
    self.storage.data.teachers = teachers_local
end

function SchoolLib:ScanTeacherTrainerSpells()
    self:Debug("ScanTeacherTrainerSpells")
    
    local spells_global = {}
    local spells_local = {}
    local spells_char = {}    
    local teachers_global = {}
    local teachers_local = {}

    if (self.storage.data.spells ~= nil) then spells_local = self:Clone(self.storage.data.spells) end
    if (self.storage.data.teachers ~= nil) then teachers_local = self:Clone(self.storage.data.teachers) end
    if (self.storage.data.player.spells["character"] ~= nil) then spells_char = self:Clone(self.storage.data.player.spells["character"]) end
    
    local npc = {}
    npc.name = self:GetTeacherName()

    local filter = {}
    filter.available = GetTrainerServiceTypeFilter("available")
    filter.unavailable = GetTrainerServiceTypeFilter("unavailable")
    filter.used = GetTrainerServiceTypeFilter("used")

    SetTrainerServiceTypeFilter("available", 1)
    SetTrainerServiceTypeFilter("unavailable", 1)
    SetTrainerServiceTypeFilter("used", 1)

    ExpandTrainerSkillLine(0)

    local numTrainerServices = GetNumTrainerServices()
    if (not numTrainerServices or (numTrainerServices < 1)) then return end
    
    self:Debug("Found " .. tostring(numTrainerServices) .. " trainer services.")

    local category = nil
    
    for i = 1, numTrainerServices do
        local continue = true
        local service = {}
        local charservice = {}

        service.name, service.subtext, charservice.type, service.expanded = GetTrainerServiceInfo(i)
        if (not service.name or not charservice.type) then continue = false end

        service.name = self.babble:SpellReverse(service.name)
        if (charservice.type == "header") then 
            category = service.name
            continue = false
        end
        
        if (continue) then
            if (category ~= nil) then service.category = category end
            if (not service.subtext) or (string.len(service.subtext) < 1) then service.subtext = "None" end
        
            service.fullname = service.name
            service.rank = 0
            if (service.subtext ~= "None") then
                local fa, fx, rank = string.find(service.subtext, "Rank (%d+)")
                service.rank = tonumber(rank) or 0
                service.fullname = service.fullname .. " (" .. service.subtext .. ")"
            end
            
            service.icon = GetTrainerServiceIcon(i)
            service.level = GetTrainerServiceLevelReq(i)

            local req = {}
            req.skill, req.rank, req.hasReq = GetTrainerServiceSkillReq(i)
            if (req.skill) then service.req = req end

            local numRequirements = GetTrainerServiceNumAbilityReq(i);
            if (numRequirements > 0) then
                service.ability = {}
                for k = 1, numRequirements do
                    service.ability[k] = {}
                    service.ability[k].skill, service.ability[k].req = GetTrainerServiceAbilityReq(i, k)
                end
            end

            service.description = GetTrainerServiceDescription(i)
            service.learnspell, service.petlearnspell = IsTrainerServiceLearnSpell(i)

            if (not service.cost) then service.cost = {} end
            -- Money, Character Point Cost 1, Character Point Cost 2
            service.cost.money, service.cost.cost1, service.cost.cost2 = GetTrainerServiceCost(i)

            spells_global = self:LoadCoursesByCategory(service.category, true) or {}
            if (not spells_global[service.fullname]) and (not spells_local[service.fullname]) then
                spells_local[service.fullname] = service
            end
            
            if (not spells_char[service.fullname]) then spells_char[service.fullname] = {} end
            if (charservice.type == "used") then charservice.known = true
            elseif (charservice.type == "available") or (charservice.type == "unavailable") then
                charservice.known = false
            end
            
            spells_char[service.fullname].known = charservice.known
            
            if (teachers_local[npc.name]) then
                if (not teachers_local[npc.name].spells) then teachers_local[npc.name].spells = {} end
                if (not teachers_local[npc.name].spells[service.name]) then teachers_local[npc.name].spells[service.name] = {} end
                if (not teachers_local[npc.name].spells[service.name].rank) or (service.rank > teachers_local[npc.name].spells[service.name].rank) then 
                    teachers_local[npc.name].spells[service.name].rank = service.rank
                end
            end
            
        end
    end

    self.storage.data.spells = spells_local
    self.storage.data.teachers = teachers_local
    self.storage.data.player.spells["character"] = spells_char
    
    self:TriggerEvent("SchoolEvent_TeacherTrainerChanged")
end



function SchoolLib:ScanTeacherTrainer()
    self:Debug("ScanTeacherTrainer")
    local spells = {}
    local trainers = {}
    local charspells = {}
    

    if (self.db.account.spells ~= nil) then spells = self.db.account.spells end
    if (self.db.class.trainers ~= nil) then trainers = self.db.class.trainers end
    if (self.db.char.spells ~= nil) then charspells = self:Clone(self.db.char.spells) end
    
    local npc = {}
    npc.name = self:GetTeacherName()
    npc.zone = self:GetTeacherZone()
    npc.subzone = self:GetTeacherSubZone()
    npc.type = self:GetTeacherType()

    local filter = {}
    filter.available = GetTrainerServiceTypeFilter("available")
    filter.unavailable = GetTrainerServiceTypeFilter("unavailable")
    filter.used = GetTrainerServiceTypeFilter("used")

    SetTrainerServiceTypeFilter("available", 1)
    SetTrainerServiceTypeFilter("unavailable", 1)
    SetTrainerServiceTypeFilter("used", 1)

    ExpandTrainerSkillLine(0)

    local numTrainerServices = GetNumTrainerServices()
    if (not numTrainerServices or (numTrainerServices < 1)) then return end
    
    self:Debug("Found %s trainer services.", numTrainerServices)

    local category = nil
    for i = 1, numTrainerServices do
        local continue = true
        local service = {}
        local charservice = {}

        service.name, service.subtext, charservice.type, service.expanded = GetTrainerServiceInfo(i)
        if (not service.name or not charservice.type) then continue = false end

        if (charservice.type == "header") then 
            if (bt) then service.name = bt[service.name] end
            category = service.name
            continue = false
        end

        if (continue) then 
            if (category ~= nil) then service.category = category end
            if (not service.subtext) or (string.len(service.subtext) < 1) then service.subtext = "None" end

            -- self:Debug("service.category ==> %s", service.category)
            -- self:Debug("service.name ==> %s", service.name)
            -- self:Debug("service.subtext ==> %s", service.subtext)
            
            if (self.babble.active) and (self.babble.spell) then service.name = self.babble.spell[service.name] end

            if (not spells[service.category]) then spells[service.category] = {} end
            if (not spells[service.category][service.name]) then spells[service.category][service.name] = {} end
            if (not spells[service.category][service.name][service.subtext]) then spells[service.category][service.name][service.subtext] = {} end

            if (not charspells[service.category]) then charspells[service.category] = {} end
            if (not charspells[service.category][service.name]) then charspells[service.category][service.name] = {} end
            if (not charspells[service.category][service.name][service.subtext]) then charspells[service.category][service.name][service.subtext] = {} end

            if (not spells[service.category][service.name][service.subtext].fullname) then
                service.fullname = service.name
            end

            if (not spells[service.category][service.name][service.subtext].rank) then
                service.rank = 0

                if (service.subtext ~= "None") then
                    local fa, fx, rank = string.find(service.subtext, "Rank (%d+)")
                    service.rank = tonumber(rank) or 0
                    service.fullname = service.fullname .. " (" .. service.subtext .. ")"
                end
            else
                service.rank = spells[service.category][service.name][service.subtext].rank
                service.fullname = spells[service.category][service.name][service.subtext].fullname
            end

            if (not spells[service.category][service.name][service.subtext].icon) then
                service.icon = GetTrainerServiceIcon(i)
            else
                service.icon = spells[service.category][service.name][service.subtext].icon
            end

            if (not spells[service.category][service.name][service.subtext].level) then
                service.level = GetTrainerServiceLevelReq(i)
            else
                service.level = spells[service.category][service.name][service.subtext].level
            end

            if (not spells[service.category][service.name][service.subtext].req) then
                local req = {}
                req.skill, req.rank, req.hasReq = GetTrainerServiceSkillReq(i)
                if (req.skill) then service.req = req end
            else
                service.req = spells[service.category][service.name][service.subtext].req
            end

            if (not spells[service.category][service.name][service.subtext].ability) then
                local numRequirements = GetTrainerServiceNumAbilityReq(i);
                if (numRequirements > 0) then
                    service.ability = {}
                    for k = 1, numRequirements do
                        service.ability[k] = {}
                        service.ability[k].skill, service.ability[k].req = GetTrainerServiceAbilityReq(i, k)
                    end
                end
            else
                service.ability = spells[service.category][service.name][service.subtext].ability
            end

            if (not spells[service.category][service.name][service.subtext].description) then
                -- Fix wrapped descriptions
                service.description = GetTrainerServiceDescription(i)
            else
                service.description = spells[service.category][service.name][service.subtext].description
            end

            if (not spells[service.category][service.name][service.subtext].learnspell) or (not spells[service.category][service.name][service.subtext].petlearnspell) then
                service.learnspell, service.petlearnspell = IsTrainerServiceLearnSpell(i)
            else
                service.learnspell = spells[service.category][service.name][service.subtext].learnspell
                service.petlearnspell = spells[service.category][service.name][service.subtext].petlearnspell                
            end

            if (not spells[service.category][service.name][service.subtext].known) then
                if (charservice.type == "used") then charservice.known = true
                elseif (charservice.type == "available") or (charservice.type == "unavailable") then 
                    charservice.known = false
                end
            else
                charservice.known = spells[service.category][service.name][service.subtext].known
            end

            if (not service.cost) then service.cost = {} end
            -- Money, Character Point Cost 1, Character Point Cost 2
            service.cost.money, service.cost.cost1, service.cost.cost2 = GetTrainerServiceCost(i)

            spells[service.category][service.name][service.subtext] = service

            if (not trainers[npc.name]) then trainers[npc.name] = {} end
            if (not trainers[npc.name].coords) then
                SetMapToCurrentZone()
                npc.coords = {}
                npc.coords.x, npc.coords.y = GetPlayerMapPosition("player")
                -- self:Debug("Coords\nx: %s\ny: %s", npc.coords.x, npc.coords.y)
            end
            trainers[npc.name] = npc
            if (not trainers[npc.name].spells) then trainers[npc.name].spells = {} end
            if (not trainers[npc.name].spells[service.name]) then trainers[npc.name].spells[service.name] = {} end
            if (not trainers[npc.name].spells[service.name].rank) then trainers[npc.name].spells[service.name].rank = service.rank
            else
                if (service.rank > trainers[npc.name].spells[service.name].rank) then
                    trainers[npc.name].spells[service.name].rank = service.rank
                end
            end

            charspells[service.category][service.name][service.subtext].known = charservice.known

            -- self:Debug("\nservice.category ==> %s\nservice.name ==> %s\nservice.subtext ==> %s\nnpc.name ==> %s", service.category, service.name, service.subtext, npc.name)
        end
    end

    self.db.account.spells = spells
    self.db.class.trainers = trainers
    self.db.char.spells = charspells

    if (not self.counters) then self.counters = {} end
    if (not self.counters.teacher) then self.counters.teacher = {} end
    self.counters.teacher.spells = 0
    self.counters.teacher.trainers = 0
    self.counters.teacher.charspells = 0
    
    if (self.db.account.spells ~= nil) then
        for k,v in pairs(self.db.account.spells) do
            if (v ~= nil) then
                for vk, vv in pairs(v) do
                    if (vv ~= nil) then
                        self.counters.teacher.spells = self.counters.teacher.spells + 1
                    end
                end
            end
        end
    end

    if (self.db.class.trainers ~= nil) then
        for k,v in pairs(self.db.class.trainers) do
            if (v ~= nil) then
                self.counters.teacher.trainers = self.counters.teacher.trainers + 1
            end
        end
    end
    
    if (self.db.char.spells ~= nil) then
        for k,v in pairs(self.db.char.spells) do
            if (v ~= nil) then
                for vk,vv in pairs(v) do
                    if (vv ~= nil) then
                        self.counters.teacher.charspells = self.counters.teacher.charspells + 1
                    end
                end
            end
        end
    end
    
    self:TriggerEvent("SchoolEvent_TeacherTrainerChanged")
end

function SchoolLib:ScanTeacherMerchant()
end

-------------------------------------------------------------------------------
-- GetData
-------------------------------------------------------------------------------
function SchoolLib:GetDataStudent()
    self:Debug("GetDataStudent")
    local data = {}
    
    data.level = self.storage.data.player.level
    data.money = self.storage.data.player.money
    data.charpoints = self.storage.data.player.charpoints
    data.spells = self.storage.data.player.spells
    data.talents = self.storage.data.player.talents
    data.tradeskills = self.storage.data.player.tradeskills
    data.skills = self.storage.data.player.skills
    data.professions = self.storage.data.player.professions
    
    data.counters = self.storage.data.player.counters
    
    self:Debug("\nskills: %s\nprofessions: %s\ntalents: %s", data.counters.skills, data.counters.professions, data.counters.talents)
    
    return data
end

function SchoolLib:GetDataTeachers()
    self:Debug("GetDataTeachers")
    local data = {}
    
    data.charspells = self.storage.data.player.spells["character"]
    data.spells = self.storage.data.spells
    data.teachers = self.storage.teachers
    -- data.counters = self.storage.counters

    return data
end

function SchoolLib:GetDataSchool()
end

-------------------------------------------------------------------------------
-- Utility
-------------------------------------------------------------------------------
function SchoolLib:GetTeacherName()
    return UnitName("npc")
end

function SchoolLib:GetTeacherZone()
    local zone = GetZoneText()
    zone = self.babble:ZoneReverse(zone)
    return zone
end

function SchoolLib:GetTeacherSubzone(teacher)
    local subzone = GetSubZoneText()
    subzone = self.babble:ZoneReverse(subzone)
    return subzone
end

function SchoolLib:GetTeacherType(teacher)
    local result
    
    if (self.tooltip) then 
        self.tooltip:SetUnit("npc")
        result = self.tooltip:GetLine(2)
    end
    
    if (not result) then
        if (not teacher) then teacher = self:GetTeacherName() end
        local teachertypes = self:LoadTeacherTypes()
        result = teachertypes[teacher]
    end

    return result
end

function SchoolLib:NPCIsTeacher(npc)
    local result = true
    local teachertypes = self:LoadTeacherTypes()
    
    if (not teachertypes[npc.name]) then result = false end
    if (not result) then
        for k, v in pairs(teachertypes) do
            if (v ~= nil) then
                if (npc.type == v) then 
                    result = true 
                    break
                end
            end
        end
    end
    
    return result
end

-------------------------------------------------------------------------------
-- Library
-------------------------------------------------------------------------------
local function activate(self, oldLib, oldDeactivate)
    SchoolLib = self
    if (oldLib) then
        self.data = oldLib.data
        self.storage = oldLib.storage

        oldLib:UnregisterAllEvents()    
        oldLib:CancelAllScheduledEvents()
    else
        self.data = {}
        self.storage = {}
    end
    
    if (oldDeactivate) then oldDeactivate(oldLib) end
end

local function external(self, major, instance)
    if (major == "AceEvent-2.0") then
        AceEvent = instance
        AceEvent:embed(self)
        self:UnregisterAllEvents()
        self:CancelAllScheduledEvents()
        if (AceEvent:IsFullyInitialized()) then
            self:AceEvent_FullyInitialized()
        else
            self:RegisterEvent("AceEvent_FullyInitialized", "AceEvent_FullyInitialized", true)
        end
    end
    
    if (major == "AceDB-2.0") then 
        AceDB = instance 
        AceDB:embed(self)     
    end
end

-------------------------------------------------------------------------------
-- Events
-------------------------------------------------------------------------------
function SchoolLib:AceEvent_FullyInitialized()
    self.inCombat = false
    self:RegisterEvent("PLAYER_REGEN_DISABLED", function() self.inCombat = true end)
    self:RegisterEvent("PLAYER_REGEN_ENABLED", function() self.inCombat = false end)
end

function SchoolLib:Event_TrainerInfo()
    self:ScanTeacherTrainerInfo()
end

function SchoolLib:Event_TrainerSpells()
    self:ScanTeacherTrainerSpells()
end

function SchoolLib:Event_MerchantInfo()
    self:ScanTeacherMerchantInfo()
end

function SchoolLib:Event_MerchantSpells()
    self:ScanTeacherMerchantSpells()
end

function SchoolLib:Event_PlayerLevelUp()
    self:ScanStudentLevel()
end

function SchoolLib:Event_Money()
    self:ScanStudentMoney()
end

function SchoolLib:Event_CharacterPoints()
    self:ScanStudentCharacterPoints()
end

function SchoolLib:Event_ChatMsgSkill()
    self:ScanStudentSkills()
end

function SchoolLib:Event_Spells()
    self:ScanStudentSpells()
end

function SchoolLib:Event_PlayerEnteringWorld()
    SchoolLib:RebuildStudent()
end

function SchoolLib:Event_SchoolBuild()
end

function SchoolLib:Event_SchoolTeacherChanged()
end

function SchoolLib:Event_SchoolStudentChanged()
end

-------------------------------------------------------------------------------
-- Data
-------------------------------------------------------------------------------

SchoolLib.teachers = {}
SchoolLib.courses = {}

function SchoolLib:LoadTeacherTypes()
    if (not SchoolLib.matrix) then return end
    return SchoolData:LoadTeacherTypeLookup()
end

function SchoolLib:LoadApprentice()
    local apprentice = {}
    apprentice.primary = {
        ["Apprentice Alchemist"] = true,
        ["Apprentice Blacksmith"] = true,
        ["Apprentice Enchanting"] = true,
        ["Apprentice Engineer"] = true,
        ["Apprentice Herbalist"] = true,
        ["Apprentice Jewelcrafting"] = true,
        ["Apprentice Miner"] = true,
        ["Apprentice Leatherworking"] = true,
        ["Apprentice Skinning"] = true,
        ["Apprentice Tailor"] = true,
    }
    apprentice.secondary = {
        ["Apprentice Cook"] = true,
        ["Apprentice First Aid"] = true,
        ["Apprentice Fishing"] = true,
    }

    return apprentice
end

function SchoolLib:LoadApprenticePrimary()
    local apprentice = self:LoadApprentice()
    if (not apprentice) then return end
    local results = {}
    results = self:Clone(apprentice.primary)
    apprentice = nil
    return results
end

function SchoolLib:LoadApprenticeSecondary()
    local apprentice = self:LoadApprentice()
    if (not apprentice) then return end
    local results = {}
    results = self:Clone(apprentice.secondary)
    apprentice = nil
    return results
end
    
function SchoolLib:LoadProffesions()
  -- Alchemy
    -- Blacksmithing
    -- Cooking
    -- Enchanting
    -- Engineering
    -- First Aid
    -- Fishing
    -- Herbalism
    -- Jewelcrafting
    -- Leatherworking
    -- Mining
    -- Skinning
    -- Tailoring 

    local professions = {}
    professions.primary = {
        ["Alchemy"] = true,
        ["Blacksmithing"] = true,
        ["Enchanting"] = true,
        ["Enginering"] = true,
        ["Herbalism"] = true,
        ["Jewelcrafting"] = true,
        ["Leatherworking"] = true,
        ["Mining"] = true,
        ["Skinning"] = true,
        ["Tailoring"] = true,
    }
    professions.secondary = {
        ["Cooking"] = true,
        ["First Aid"] = true,
        ["Fishing"] = true,
    }
    
    return professions
end

function SchoolLib:LoadProfessionsPrimary()
    local professions = self:LoadProfessions()
    if (not professions) then return end
    local results = {}
    results = self:Clone(professions.primary)
    professions = nil
    return results
end

function SchoolLib:LoadProfessionsSecondary()
    local professions = self:LoadProfessions()
    if (not professions) then return end
    local results = {}
    results = self:Clone(professions.secondary)
    professions = nil
    return results
end

function SchoolLib:LoadTeachers()
    if (not SchoolLib.matrix) then return end
    return SchoolLib.matrix:LoadTeachers()
end

function SchoolLib:LoadCourses()
    if (not SchoolData) then return end
    return SchoolData:LoadCourses()
end

function SchoolLib:LoadTeachersByClass(class, alpha)
    local teachers = self:LoadTeachers()
    local results = {}

    if (not teachers) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k,v in ipairs(teachers) do
        if (v ~= nil) then
            if (not v.class) or (v.class == "") or (v.class == "None") or (v.class == class) then
                if (not alpha) then table.insert(results, v)
                else 
                    if (not results[v.name]) then results[v.name] = {} end
                    results[v.name] = v
                end
            end
        end
    end

    teachers = nil
    return results
end

function SchoolLib:LoadTeachersByZone(zone, alpha)
    local teachers = self:LoadTeachers()
    local results = {}

    if (not teachers) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k,v in ipairs(teachers) do
        if (v ~= nil) then
            if (not v.zone) or (v.zone == "") or (v.zone == "None") or (v.zone == zone) then
                if (not alpha) then table.insert(results, v)
                else 
                    if (not results[v.name]) then results[v.name] = {} end
                    results[v.name] = v
                end
            end
        end
    end

    teachers = nil
    return results
end

function SchoolLib:LoadTeachersBySubzone(subzone, alpha)
    local teachers = self:LoadTeachers()
    local results = {}
    
    if (not teachers) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k,v in ipairs(teachers) do
        if (v ~= nil) then
            if (not v.subzone) or (v.subzone == "") or (v.subzone == "None") or (v.subzone == subzone) then
                if (not alpha) then table.insert(results, v)
                else
                    if (not results[v.name]) then results[v.name] = {} end
                    results[v.name] = v
                end
            end
        end
    end
    
    teachers = nil
    return results
end

function SchoolLib:LoadCoursesByCategory(category, alpha)
    local courses = self:LoadCourses()
    local results = {}

    if (not teachers) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k,v in ipairs(courses) do
        if (v ~= nil) then
            if (v.category == category) then
                if (not alpha) then table.insert(results, v)
                else
                    if (not results[v.fullname]) then results[v.fullname] = {} end
                    results[v.fullname] = v
                end
            end
        end
    end

    courses = nil
    return results
end

function SchoolLib:LoadCoursesByName(name, alpha)
    local courses = self:LoadCourses()
    local results = {}

    if (not courses) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k,v in pairs(courses) do
        if (v ~= nil) then
            if (v.name == name) then
                if (not alpha) then table.insert(results, v)
                else
                    if (not results[v.fullname]) then results[v.fullname] = {} end
                    results[v.fullname] = v
                end
            end
        end
    end
    
    courses = nil
    return results
end    

function SchoolLib:LoadCoursesByFullname(fullname, alpha)
    local courses = self:LoadCourses()
    local results = {}

    if (not courses) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k, v in ipairs(courses) do
        if (v ~= nil) then
            if (v.fullname == fullname) then
                if (not alpha) then table.insert(results, v)
                else
                    if (not results[v.fullname]) then results[v.fullname] = {} end
                    results[v.fullname] = v
                end
            end
        end
    end
    
    courses = nil
    return results
end

function SchoolLib:LoadCoursesByRank(rank, alpha)
    local courses = self:LoadCourses()
    local results = {}

    if (not courses) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k,v in pairs(courses) do
        if (v ~= nil) then
            if (v.rank == rank) then
                if (not alpha) then table.insert(results, v)
                else
                    if (not results[v.fullname]) then results[v.fullname] = {} end
                    results[v.fullname] = v
                end
            end
        end
    end
    
    courses = nil
    return results
end

function SchoolLib:LoadCoursesByLevel(level, alpha)
    local courses = self:LoadCourses()
    local results = {}

    if (not courses) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k,v in pairs(courses) do
        if (v ~= nil) then
            if (v.level == level) then
                if (not alpha) then table.insert(results, v)
                else
                    if (not results[v.fullname]) then results[v.fullname] = {} end
                    results[v.fullname] = v
                end
            end
        end
    end
    
    courses = nil
    return results
end

function SchoolLib:LoadCoursesBySkill(skill, alpha)
    local courses = self:LoadCourses()
    local results = {}

    if (not courses) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k, v in ipairs(courses) do
        if (v ~= nil) and (v.ability ~= nil) then
            for ak, av in ipairs(v.ability) do
                if (av ~= nil) then
                    if (av.skill == skill) then
                        if (not alpha) then table.insert(results, v)
                        else
                            if (not results[v.fullname]) then results[v.fullname] = {} end
                            results[v.fullname] = v
                        end
                    end
                end
            end
        end
    end
    
    courses = nil
    return results
end

function SchoolLib:LoadCoursesByCost(cost, alpha)
    local courses = self:LoadCourses()
    local results = {}

    if (not courses) then return end
    if (type(alpha) ~= "boolean") then alpha = false end
    
    for k, v in ipairs(courses) do
        if (v ~= nil) and (v.cost ~= nil) then
            if (v.cost.money ~= nil) and (v.cost.money == cost) then
                if (not alpha) then table.insert(results, v)
                else
                    if (not results[v.fullname]) then results[v.fullname] = {} end
                    results[v.fullname] = v
                end
            end
        end
    end
    
    courses = nil
    return results
end
            
-------------------------------------------------------------------------------
-- Load
-------------------------------------------------------------------------------
AceLibrary:Register(SchoolLib, MAJOR_VERSION, MINOR_VERSION, activate, nil, external)
